package plsqlfs;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.google.common.annotations.VisibleForTesting;

public class FileSystem {



	private static final String LINE_TERMINATOR = "\n";

	private static int nextFileHandleId = 1;



	private Interpolator interpolator;

	public FileSystem() {
		this( new SystemEnvironmentInterpolator() );
	}

	public FileSystem(Interpolator interpolator) {
		this.interpolator = interpolator;
	}

	private Charset getDefaultCharset() {
		return Charset.defaultCharset();
	}

	@VisibleForTesting
	protected String interpolate(String string) {
		return interpolator.interpolateFully( string );
	}

	@VisibleForTesting
	protected Path resolvePath(String unresolvedPath) {
		File resolvedPath = new File(interpolate(unresolvedPath));
		return new Path(resolvedPath, unresolvedPath);
	}

	private Path resolveExistingPath(String path) throws FileSystemException {
		Path resolvedPath = resolvePath(path);
		
		if (!resolvedPath.exists()) {
			throw new FileSystemException( FileSystemException.FILE_NOT_FOUND, path + " does not exist" );
		}
		
		return resolvedPath;
	}

	@VisibleForTesting
	protected Path resolveFilePath(String filePath) {
		Path resolvedPath = resolvePath(filePath);
		
		if (!resolvedPath.isFile()) {
			if (resolvedPath.isDirectory()) {
				throw new NonFileException(filePath + " is a directory, not a file");
			}
			
			if (resolvedPath.exists()) {
				throw new NonFileException(filePath + " is not a file");
			}
		}
		
		return resolvedPath;
	}

	@VisibleForTesting
	protected Path resolveExistingFilePath(String filePath) {
		Path resolvedFilePath = resolveFilePath(filePath);
		
		if (!resolvedFilePath.exists()) {
			throw new FileNotFoundException("File not found: " + filePath);
		}
		
		return resolvedFilePath;
	}

	@VisibleForTesting
	protected Path resolveExistingDirectoryPath(String directoryPath) {
		Path resolvedDirectoryPath = resolveDirectoryPath(directoryPath);
		
		if (!resolvedDirectoryPath.exists()) {
			throw new DirectoryNotFoundException("Directory not found: " + directoryPath);
		}
		
		return resolvedDirectoryPath;
	}

	@VisibleForTesting
	protected Path resolveDirectoryPath(String directoryPath) {
		Path resolvedPath = resolvePath(directoryPath);
		
		if (!resolvedPath.isDirectory()) {
			if (resolvedPath.isFile()) {
				throw new NonFileException(directoryPath + " is a file, not a directory");
			}
			
			if (resolvedPath.exists()) {
				throw new NonFileException(directoryPath + " is not a directory");
			}
		}
		
		return resolvedPath;
	}

	@VisibleForTesting
	protected Path resolveExistentDirectoryPath(String directoryPath) {
		Path resolvedDirectoryPath = resolveDirectoryPath(directoryPath);
		
		if (!resolvedDirectoryPath.exists()) {
			throw new FileNotFoundException();
		}
		
		return resolvedDirectoryPath;
	}

	public String appendToPath(String basePath, String relativePath) {
		return new File(basePath, relativePath).getPath();
	}



	public String base(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1) {
			return name;
		} else {
			return name.substring(0, index);
		}
	}

	public String extension(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1) {
			return "";
		} else {
			return name.substring(index);
		}
	}

	public String resolvedPath(String path) {
		return resolvePath(path).getPath();
	}

	public String absolutePath(String path) {
		return resolvePath(path).getAbsolutePath();
	}

	public String canonicalPath(String path) throws IOException {
		return resolvePath(path).getCanonicalPath();
	}



	public String resolvedFilePath(String filePath) {
		return resolveFilePath(filePath).getPath();
	}

	public String absoluteFilePath(String filePath) {
		return resolveFilePath(filePath).getAbsolutePath();
	}

	public String canonicalFilePath(String filePath) throws IOException {
		return resolveFilePath(filePath).getCanonicalPath();
	}



	public String resolvedDirectoryPath(String directoryPath) {
		return resolveDirectoryPath(directoryPath).getPath();
	}

	public String absoluteDirectoryPath(String directoryPath) {
		return resolveDirectoryPath(directoryPath).getAbsolutePath();
	}

	public String canonicalDirectoryPath(String directoryPath) throws IOException {
		return resolveDirectoryPath(directoryPath).getCanonicalPath();
	}



	public String name(String path) throws FileSystemException {
		return resolvePath(path).getName();
	}

	public String fileName(String filePath) throws FileSystemException {
		return resolveFilePath(filePath).getName();
	}

	public String directoryName(String directoryPath) throws FileSystemException {
		return resolveDirectoryPath(directoryPath).getName();
	}

	public long fileSize(String filePath) {
		return resolveExistingFilePath(filePath).length();
	}

	public boolean exists(String path) throws FileSystemException {
		return resolvePath(path).exists();
	}

	public boolean fileExists(String filePath) throws FileSystemException {
		return resolveFilePath(filePath).exists();
	}

	public void delete(String path) throws FileSystemException {
		if (!resolveExistingPath(path).delete()) {
			throw new FileSystemException( FileSystemException.COULD_NOT_DELETE, "Could not delete " + path );
		}
	}



	protected void copyFile(File sourceFile, File destinationFile) throws FileSystemException, IOException {
		ByteStreams.copy( sourceFile, destinationFile );
		// TODO explicitly handle all the exceptions ourselves
	}



	public void copyFileToFile(String sourceFilePath, String destinationFilePath) throws FileSystemException, IOException {
		Path sourceFile = resolveExistingFilePath(sourceFilePath);
		Path destinationFile = resolveFilePath(destinationFilePath);
		
		copyFile(sourceFile.resolvedPath, destinationFile.resolvedPath);
	}



	/**
	 * 
	 * @param sourceFilePath
	 * @param destinationDirectoryPath
	 * @param destinationFilePath
	 * @param errorCode
	 * @param errorMessage
	 * @return
	 * @throws FileSystemException 
	 * @throws IOException 
	 */
	public String copyFileToDirectory(String sourceFilePath, String destinationDirectoryPath) throws FileSystemException, IOException {
		Path sourceFile = resolveExistingPath(sourceFilePath);
		Path destinationDirectory = resolveExistingPath(destinationDirectoryPath);
		
		if ( !destinationDirectory.isDirectory() ) {
			// TODO throw new FileSystemException(  );
		}
		
		File destinationFile = new File(destinationDirectory.resolvedPath, sourceFile.getName());
		
		copyFile(sourceFile.resolvedPath, destinationFile);
		
		return destinationFile.toString();
	}



	protected void moveFile(File sourceFile, File destinationFile) throws FileSystemException, IOException {
		//ByteStreams.copy( sourceFile, destinationFile );
		// TODO explicitly handle all the exceptions ourselves
		sourceFile.renameTo(destinationFile);
	}



	public void moveFileToFile(String sourceFilePath, String destinationFilePath) throws FileSystemException, IOException {
		Path sourceFile = resolveExistingFilePath(sourceFilePath);
		Path destinationFile = resolveFilePath(destinationFilePath);
		
		moveFile(sourceFile.resolvedPath, destinationFile.resolvedPath);
	}



	public String moveFileToDirectory(String sourceFilePath, String destinationDirectoryPath) throws FileSystemException, IOException {
		Path sourceFile = resolveExistingFilePath(sourceFilePath);
		Path destinationDirectory = resolveExistingPath(destinationDirectoryPath);
		
		if (!destinationDirectory.isDirectory()) {
			// TODO throw new FileSystemException(  );
		}
		
		File destinationFile = new File(destinationDirectory.resolvedPath, sourceFile.getName());
		
		moveFile(sourceFile.resolvedPath, destinationFile);
		
		return destinationFile.toString();
	}



	public void touch(String path) throws FileSystemException, IOException {
		resolveExistingPath(path).setLastModified(System.currentTimeMillis());
	}

	public void touchFile(String filePath) throws FileSystemException, IOException {
		Path resolvedPath = resolvePath(filePath);
		if (!resolvedPath.createNewFile()) {
			resolvedPath.setLastModified(System.currentTimeMillis());
		}
	}

	public void touchDirectory(String directoryPath) throws FileSystemException, IOException {
		Path directory = resolveDirectoryPath(directoryPath);
		if (directory.exists()) {
			directory.setLastModified(System.currentTimeMillis());
		} else {
			boolean directoriesCreated = directory.mkdirs();
			if (!directoriesCreated) {
				throw new FileSystemException(FileSystemException.UNKNOWN, "Could not create directory");
			}
		}
	}

	public boolean isAbsolute(String path) {
		return resolvePath(path).isAbsolute();
	}

	public boolean isDirectory(String path) {
		return resolvePath(path).isDirectory();
	}

	public boolean isFile(String path) {
		return resolvePath(path).isFile();
	}

	@VisibleForTesting
	protected List<String> names(Collection<? extends File> files) {
		List<String> names = new ArrayList<String>(files.size());
		
		for (File file : files) {
			names.add(file.getName());
		}
		
		return Collections.unmodifiableList(names);
	}

	private List<String> paths(Collection<? extends File> files) {
		List<String> paths = new ArrayList<String>(files.size());
		
		for (File file : files) {
			paths.add(file.getPath());
		}
		
		return Collections.unmodifiableList(paths);
	}



	public List<String> listNames(String directoryPath) {
		return names(resolveExistingDirectoryPath(directoryPath).list());
	}

	public List<String> listNames(String directoryPath, String pattern) {
		return names(resolveExistingDirectoryPath(directoryPath).list(FileFilters.forPattern(pattern)));
	}

	public List<String> listPaths(String directoryPath) {
		return paths(resolveExistingDirectoryPath(directoryPath).list());
	}

	public List<String> listPaths(String directoryPath, String pattern) {
		return paths(resolveExistingDirectoryPath(directoryPath).list(FileFilters.forPattern(pattern)));
	}



	public List<String> listFileNames(String directoryPath) {
		return names(resolveExistingDirectoryPath(directoryPath).listFiles());
	}

	public List<String> listFileNames(String directoryPath, String pattern) {
		return names(resolveExistingDirectoryPath(directoryPath).listFiles(FileFilters.forPattern(pattern)));
	}

	public List<String> listFilePaths(String directoryPath) {
		return paths(resolveExistingDirectoryPath(directoryPath).listFiles());
	}

	public List<String> listFilePaths(String directoryPath, String pattern) {
		return paths(resolveExistingDirectoryPath(directoryPath).listFiles(FileFilters.forPattern(pattern)));
	}



	public List<String> listDirectoryNames(String directoryPath) {
		return names(resolveExistingDirectoryPath(directoryPath).listDirectories());
	}

	public List<String> listDirectoryNames(String directoryPath, String pattern) {
		return names(resolveExistingDirectoryPath(directoryPath).listDirectories(FileFilters.forPattern(pattern)));
	}

	public List<String> listDirectoryPaths(String directoryPath) {
		return paths(resolveExistingDirectoryPath(directoryPath).listDirectories());
	}

	public List<String> listDirectoryPaths(String directoryPath, String pattern) {
		return paths(resolveExistingDirectoryPath(directoryPath).listDirectories(FileFilters.forPattern(pattern)));
	}



	public List<String> listRootDirectoryPaths() {
		return paths(Arrays.asList(File.listRoots()));
	}



	public String[] readLines(String filePath, String charsetName) throws IOException {
		Path path = resolveExistingFilePath(filePath);
		
		InputStream inputStream = path.openInputStream();
		
		try {
			Reader reader;
			
			if (charsetName == null) {
				reader = new InputStreamReader(inputStream, getDefaultCharset());
			} else {
				reader = new InputStreamReader(inputStream, charsetName);
			}
			
			try {
				BufferedReader bufferedReader = new BufferedReader(reader);
				try {
					List<String> lines = new ArrayList<String>();
					
					String line;
					while ((line = bufferedReader.readLine()) != null) {
						lines.add(line);
					}
					
					return lines.toArray(new String[lines.size()]);
				} finally {
					bufferedReader.close();
				}
			} finally {
				reader.close();
			}
		} finally {
			inputStream.close();
		}
	}



	public abstract class FileHandle {

		private final int fileHandleId;

		public FileHandle(int fileHandleId) {
			this.fileHandleId = fileHandleId;
		}

		public int getFileHandleId() {
			return fileHandleId;
		}

		public boolean hasMoreLines() throws IOException {
			throw new UnsupportedOperationException();
		}

		public String readLine() throws IOException {
			throw new UnsupportedOperationException();
		}

		public void write(String text) throws IOException {
			throw new UnsupportedOperationException();
		}

		public void writeLine(String text) throws IOException {
			throw new UnsupportedOperationException();
		}

		public abstract void flush() throws IOException;

		public abstract void close() throws IOException;

	}

	private abstract class ReadableFileHandle extends FileHandle {

		public ReadableFileHandle(int fileHandleId) {
			super(fileHandleId);
		}

		public void flush() throws IOException {
			// By definition, readable file handles cannot flush
		}

	}

	private class BinaryReadableFileHandle extends ReadableFileHandle {

		private final InputStream inputStream;

		public BinaryReadableFileHandle(int fileHandleId, InputStream inputStream) {
			super(fileHandleId);
			this.inputStream = inputStream;
		}

		public void close() throws IOException {
			inputStream.close();
		}

	}

	private class TextualReadableFileHandle extends ReadableFileHandle {

		private final BufferedReader reader;
		private Boolean hasMoreLines;
		private String nextLine;

		public TextualReadableFileHandle(int fileHandleId, BufferedReader reader) {
			super(fileHandleId);
			this.reader = reader;
		}

		public boolean hasMoreLines() throws IOException {
			if (hasMoreLines == null) {
				nextLine = reader.readLine();
				hasMoreLines = (nextLine != null);
			}
			return hasMoreLines;
		}

		public String readLine() throws IOException {
			if (!hasMoreLines() /* this will populate nextLine if successful */) {
				throw new FileSystemException(-20117, "There are no more lines to be read");
			}
			
			String line = nextLine;
			hasMoreLines = null; // unknown
			return line;
		}

		public void close() throws IOException {
			reader.close();
		}

	}

	private abstract class WritableFileHandle extends FileHandle {

		public WritableFileHandle(int fileHandleId) {
			super(fileHandleId);
		}

	}

	private class BinaryWritableFileHandle extends WritableFileHandle {

		private final OutputStream outputStream;

		public BinaryWritableFileHandle(int fileHandleId, OutputStream outputStream) {
			super(fileHandleId);
			this.outputStream = outputStream;
		}

		public void flush() throws IOException {
			outputStream.flush();
		}

		public void close() throws IOException {
			outputStream.close();
		}

	}

	private class TextualWritableFileHandle extends WritableFileHandle {

		private final Writer writer;

		public TextualWritableFileHandle(int fileHandleId, Writer writer) {
			super(fileHandleId);
			this.writer = writer;
		}

		public void write(String text) throws IOException {
			writer.write( text );
		}

		public void writeLine(String text) throws IOException {
			writer.write( text + LINE_TERMINATOR );
		}

		public void flush() throws IOException {
			writer.flush();
		}

		public void close() throws IOException {
			writer.close();
		}

	}



	private int newFileHandleId() {
		return nextFileHandleId++;
	}



	public FileHandle openBinaryFileForReading(String filePath, int bufferSize) throws IOException {
		Path resolvedPath = resolvePath( filePath );
		
		InputStream inputStream = resolvedPath.openInputStream();
		
		BufferedInputStream bufferedInputStream;
		if (bufferSize > 0) {
			bufferedInputStream = new BufferedInputStream(inputStream, bufferSize);
		} else {
			bufferedInputStream = new BufferedInputStream(inputStream);
		}
		
		return new BinaryReadableFileHandle(newFileHandleId(), bufferedInputStream);
	}



	public FileHandle openBinaryFileForWriting(String filePath, int bufferSize) throws IOException {
		Path resolvedPath = resolvePath(filePath);
		
		OutputStream outputStream = resolvedPath.openOutputStream();
		
		BufferedOutputStream bufferedOutputStream;
		if (bufferSize > 0) {
			bufferedOutputStream = new BufferedOutputStream(outputStream, bufferSize);
		} else {
			bufferedOutputStream = new BufferedOutputStream(outputStream);
		}
		
		return new BinaryWritableFileHandle(newFileHandleId(), bufferedOutputStream);
	}



	public FileHandle openTextualFileForReading(String filePath, String charsetName, int bufferSize) throws IOException {
		Path resolvedPath = resolvePath(filePath);
		
		InputStream inputStream = resolvedPath.openInputStream();
		
		Reader reader;
		if (charsetName == null) {
			reader = new InputStreamReader(inputStream, getDefaultCharset());
		} else {
			reader = new InputStreamReader(inputStream, charsetName);
		}
		
		BufferedReader bufferedReader;
		if (bufferSize > 0) {
			bufferedReader = new BufferedReader(reader, bufferSize);
		} else {
			bufferedReader = new BufferedReader(reader);
		}
		
		return new TextualReadableFileHandle(newFileHandleId(), bufferedReader);
	}



	public FileHandle openTextualFileForWriting(String filePath, String charsetName, int bufferSize) throws IOException {
		Path resolvedPath = resolvePath(filePath);
		
		OutputStream outputStream = resolvedPath.openOutputStream();
		
		Writer writer;
		if (charsetName == null) {
			writer = new OutputStreamWriter(outputStream, getDefaultCharset());
		} else {
			writer = new OutputStreamWriter(outputStream, charsetName);
		}
		
		BufferedWriter bufferedWriter;
		if (bufferSize > 0) {
			bufferedWriter = new BufferedWriter(writer, bufferSize);
		} else {
			bufferedWriter = new BufferedWriter(writer);
		}
		
		return new TextualWritableFileHandle(newFileHandleId(), bufferedWriter);
	}



	public void closeFileHandle(FileHandle fileHandle) throws IOException {
		fileHandle.close();
	}


}
